/*
 * Background Flickr: Copyright (C) 2009  Craig McNicholas
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.ddw.app.bflickr.flickr.api.request;

import java.io.IOException;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import com.ddw.app.bflickr.flickr.api.FlickrAccount;
import com.ddw.app.bflickr.flickr.api.FlickrURL;
import com.ddw.app.bflickr.flickr.api.FlickrResponse.FlickrResponseErrorCode;
import com.ddw.app.bflickr.flickr.api.response.FlickrFindResponse;
import com.ddw.app.bflickr.flickr.api.url.FlickrFindByEmailURL;
import com.ddw.app.bflickr.flickr.api.url.FlickrFindByUsernameURL;
import com.ddw.app.bflickr.flickr.api.url.FlickrFindByWebsiteURL;
import com.ddw.app.bflickr.flickr.api.url.FlickrFindURL;
import com.ddw.app.bflickr.flickr.api.utils.XmlUtils;
import com.ddw.app.bflickr.logging.Log;
import com.ddw.app.bflickr.logging.LogFactory;

/**
 * Class to request, parse and respond to a flickr find 
 * service call. The request is made asynchronously so 
 * listeners must be in place to catch the response.
 * @author craig
 *
 */
public class FlickrFindRequest extends FlickrApiRequest {

	/**
	 * Enumeration for the flickr response xml tag names.
	 * <br/><b>ERROR</b> The error tag if present in the xml document
	 * <br/><b>USER</b> The user tag if present in the xml document
	 * <br/><b>USER_USERNAME</b> The username tag which is a child of USER if present in the xml document
	 * @author craig
	 *
	 */
	private enum FlickrResponseTagName {
		ERROR (),
		USER (),
		USER_USERNAME ();
		
		private Log log = LogFactory.createInstance(this.getClass());
		
		/**
		 * Getter of the tag name for the enumerated value
		 * @return the tag name
		 */
		public String getTag() {
			switch(this) {
			case ERROR: return "err";
			case USER: return "user";
			case USER_USERNAME: return "username";
			}
			log.error("Could not assert the value of the enumerated type " + this);
			throw new AssertionError("Could not assert the value of the enumerated type " + this);
		}
	}
	
	/**
	 * Enumeration for the flickr response xml attribute names.
	 * <br/><b>ERROR_CODE</b> The error code attribute of the ERROR tag if present 
	 * in the xml document, <i>this is an integer</i>
	 * <br/><b>ERROR_MESSAGE</b> The message attribute of the ERROR tag if present 
	 * in the xml document
	 * <br/><b>USER_ID</b> The NSID attribute of the USER tag if present 
	 * in the xml document
	 * @author craig
	 *
	 */
	private enum FlickrResponseAttributeName {
		ERROR_CODE (),
		ERROR_MESSAGE (),
		USER_ID ();
		
		private Log log = LogFactory.createInstance(this.getClass());
		
		/**
		 * Getter of the attribute name for the enumerated value
		 * @return the attribute name
		 */
		public String getTag() {
			switch(this) {
			case ERROR_CODE: return "code";
			case ERROR_MESSAGE: return "msg";
			case USER_ID: return "id";
			}
			log.error("Could not assert the value of the enumerated type " + this);
			throw new AssertionError("Could not assert the value of the enumerated type " + this);
		}
	}
	
	private Log log = LogFactory.createInstance(this.getClass());
	
	/**
	 * Creates a new find api request instance to request a response from a 
	 * REST url and provide callbacks to handle to returned data. The response 
	 * should return user information after querying the service.
	 * @param url the url to request a response from
	 * @throws IllegalArgumentException when the url is null, <i>url's cannot be null</i>
	 */
	public FlickrFindRequest(FlickrFindURL url) throws IllegalArgumentException {
		super(url);
	}

	@Override
	protected FlickrFindResponse parseResponse(String response) throws Throwable {
		log.debug("Beginning parse of response string");
		XmlPullParser parser = XmlUtils.createXmlParser(response);
		FlickrFindResponse res = new FlickrFindResponse();
		parseXml(parser, res);
		log.debug("Finished parsing. Reached end of document");
		return res;
	}

	/**
	 * Method to loop through the xml pull parser line by line and attempt to
	 * read the response from a flickr find service
	 * @param parser the pull parser instance
	 * @param res the response object to return to listening objects
	 * @throws XmlPullParserException when the pull parser encounters a problem reading the xml
	 * @throws IOException if the string buffer fails
	 */
	private void parseXml(XmlPullParser parser, FlickrFindResponse res) throws XmlPullParserException, IOException {
		boolean inUsernameTag = false;
		int event = parser.getEventType();
		while(event != XmlPullParser.END_DOCUMENT) { // Loop through nodes catching certain elements
			switch(event) {
			case XmlPullParser.START_TAG:
				inUsernameTag = parseXmlStartTag(parser, res, inUsernameTag);
				break;
				
			case XmlPullParser.END_TAG:
				inUsernameTag = parseXmlEndTag(parser, inUsernameTag);
				break;
				
			case XmlPullParser.TEXT:
				parseXmlText(parser, res, inUsernameTag);
				break;
				
			}
			event = parser.next();
		}
	}

	/**
	 * Method to parse an xml text element. This method looks for username text.
	 * @param parser the pull parser instance
	 * @param res the response object to return to listening objects
	 * @param inUsernameTag if the parser is currently inside the username tag
	 */
	private void parseXmlText(XmlPullParser parser, FlickrFindResponse res, boolean inUsernameTag) {
		if(inUsernameTag) {
			log.debug("Parser found TEXT in USER_USERNAME tag.");
			if(res.getUser() != null) {
				log.debug("Parser found instance of flickr account, setting username");
				res.getUser().setUsername(parser.getText());
			} else {
				log.error("Error parsing, could not find instance of flickr account, did not set username");
			}
		} else {
			log.debug("Parser found TEXT but not inside the USER_USERNAME tag.");
		}
	}

	/**
	 * Method to parse an xml closing tag. This method looks for the end username 
	 * tag to update the flag.
	 * @param parser the xml pull parser instance
	 * @param inUsernameTag if the parser is currently inside the username tag
	 * @return a flag indicating whether the parser has moved past the username tag
	 */
	private boolean parseXmlEndTag(XmlPullParser parser, boolean inUsernameTag) {
		for(FlickrResponseTagName tag : FlickrResponseTagName.values()) {
			if(tag.getTag().compareToIgnoreCase(parser.getName()) == 0) {
				switch(tag) {
				case USER_USERNAME:
					log.debug("Parser found USER_USERNAME end tag.");
					inUsernameTag = false;
					break;
				}
			}
		}
		return inUsernameTag;
	}

	/**
	 * Method to parse an xml starting tag. This method looks for error, user and username 
	 * tags so that it can call the correct parsing methods to handle attributes etc.
	 * @param parser the pull parser instance
	 * @param res the response object to return to listening objects 
	 * @param inUsernameTag if the parser is currently inside the username tag
	 * @return a flag indicating whether the parser has moved into the username tag
	 */
	private boolean parseXmlStartTag(XmlPullParser parser, FlickrFindResponse res, boolean inUsernameTag) {
		for(FlickrResponseTagName tag : FlickrResponseTagName.values()) {
			if(tag.getTag().compareToIgnoreCase(parser.getName()) == 0) {
				switch(tag) {
				case ERROR:
					parseXmlStartErrorTag(parser, res);
					break;
				case USER:
					parseXmlStartUserTag(parser, res);
					break;
				case USER_USERNAME:
					log.debug("Parser found USER_USERNAME start tag.");
					inUsernameTag = true;
					break;
				}
			}
		}
		return inUsernameTag;
	}

	/**
	 * Method to parse the starting user tag. This method attempts to retrieve the 
	 * users unique id.
	 * @param parser the pull parser instance
	 * @param res the response object to return to listening objects 
	 */
	private void parseXmlStartUserTag(XmlPullParser parser, FlickrFindResponse res) {
		log.debug("Parser found USER start tag.");
		res.setResponseCode(FlickrResponseErrorCode.SUCCESS); // If there is a user tag the request was successful
		String NSID = parser.getAttributeValue("", FlickrResponseAttributeName.USER_ID.getTag());
		FlickrAccount acc = createUser(NSID);
		if(acc != null) {
			res.setUser(acc);
		}
	}

	/**
	 * Method to parse an NSID and return a flickr account with the NSID, also setting
	 * the username or email address if the calling url was of the right type. Example 
	 * if the url used to call the service included email information this will be set.
	 * @param NSID the found unique id
	 * @return A flickr account generated from the NSID if valid
	 */
	private FlickrAccount createUser(String NSID) {
		if(NSID != null) {
			if(NSID.trim().length() > 0) {
				Class<? extends FlickrURL> c = this.getUrl().getClass();
				if(c == FlickrFindByEmailURL.class) {
					return new FlickrAccount(NSID, ((FlickrFindByEmailURL) this.getUrl()).getEmailAddress());
				} else if(c == FlickrFindByWebsiteURL.class) {
					return new FlickrAccount(NSID);
				} else if(c == FlickrFindByUsernameURL.class) {
					return new FlickrAccount(NSID, ((FlickrFindByUsernameURL) this.getUrl()).getUsername());
				} else {
					log.error("Error parsing, the flickr url is not a valid FlickrFindURL");
				}
			} else {
				log.error("Error parsing, the NSID was empty");
			}
		} else {
			log.error("Error parsing, the NSID was null");
		}
		return null;
	}

	/**
	 * Method to parse the starting error tag. This method attempts to retrieve 
	 * the error code sent with the error tag and update the response code.
	 * @param parser the pull parser instance
	 * @param res the response object to return to listening objects 
	 */
	private void parseXmlStartErrorTag(XmlPullParser parser, FlickrFindResponse res) {
		log.debug("Parser found ERROR start tag.");
		String errorCode = parser.getAttributeValue("", FlickrResponseAttributeName.ERROR_CODE.getTag());
		FlickrResponseErrorCode code = FlickrResponseErrorCode.parseCode(errorCode);
		res.setResponseCode(code);
	}

}
